﻿// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Copyright (c) 2011, Dr. Masroor Ehsan
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the author nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// 
// $Author:$
// $Id:$
// $Rev:$
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

using System;
using System.Collections.Generic;
using System.Globalization;
using CuttingEdge.Conditions;

namespace MaxTrader.Core
{
    public sealed class TradingDate : IComparable<TradingDate>, ICloneable, IParameterizedInstance<TradingDate, DateTime>
    {
        private readonly DateTime _currDate;
        private readonly int _day;
        private readonly int _month;
        private readonly int _year;

        /// <summary>
        ///   Create a new date from the given year, month and day
        /// </summary>
        /// <param name = "year">a four digit year, e.g. 1996</param>
        /// <param name = "month">the month starting from 1</param>
        /// <param name = "day">the day of month starting from 1</param>
        public TradingDate(int year, int month, int day)
        {
            Condition.Requires(IsValidDate(year, month, day)).IsNotEqualTo(false);

            _year = year;
            _month = month;
            _day = day;
            _currDate = DateTime.SpecifyKind(new DateTime(_year, _month, _day), DateTimeKind.Utc);
        }

        /// <summary>
        ///   Create a new date from the given DateTime.
        /// </summary>
        public TradingDate(DateTime date) : this(date.Year, date.Month, date.Day)
        {
        }

        /// <summary>
        ///   Create a new date set to today (UTC time)
        /// </summary>
        public TradingDate()
            : this(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day)
        {
        }

        public int Year
        {
            get { return _year; }
        }

        public int Month
        {
            get { return _month; }
        }

        public int Day
        {
            get { return _day; }
        }

        public DayOfWeek DayOfWeek
        {
            get { return _currDate.DayOfWeek; }
        }

        public int DayOfYear
        {
            get { return _currDate.DayOfYear; }
        }

        public DateTime Date
        {
            get { return _currDate; }
        }

        #region ICloneable Members

        public object Clone()
        {
            return new TradingDate(_year, _month, _day);
        }

        #endregion

        #region IComparable<TradingDate> Members

        public int CompareTo(TradingDate other)
        {
            if (_year < other.Year) return -1;
            if (_year > other.Year) return 1;

            if (_month < other.Month) return -1;
            if (_month > other.Month) return 1;

            if (_day < other.Day) return -1;
            if (_day > other.Day) return 1;

            return 0;
        }

        #endregion

        public override int GetHashCode()
        {
            return (_day + _month*256 + _year*65536);
        }

        /// <summary>
        ///   Tests if this date is before the specified date
        /// </summary>
        public bool Before(TradingDate other)
        {
            return CompareTo(other) < 0;
        }

        /// <summary>
        ///   Tests if this date is after the specified date
        /// </summary>
        public bool After(TradingDate other)
        {
            return CompareTo(other) > 0;
        }

        /// <summary>
        ///   Tests if this date is equal to the specified date
        /// </summary>
        public bool Equal(TradingDate other)
        {
            return CompareTo(other) == 0;
        }

        public bool IsWeekend()
        {
            return CalendarUtils.IsWeekend(_currDate);
        }

        private TradingDate AddDays(int days, double increment)
        {
            var date = _currDate;
            for (var i = 0; i < days; i++)
            {
                do
                {
                    date = date.AddDays(increment);
                } while (CalendarUtils.IsWeekend(date));
            }

            return new TradingDate(date);
        }

        /// <summary>
        ///   Create a new date which is the specified number of trading days before this date.
        /// </summary>
        /// <param name = "days">the number of days to move</param>
        /// <returns>date which is days before the current one</returns>
        public TradingDate Previous(int days)
        {
            return AddDays(days, -1);
        }

        /// <summary>
        ///   Create a new date which is the specified number of trading days after this date.
        /// </summary>
        /// <param name = "days">the number of days to move</param>
        /// <returns>date which is days after the current one</returns>
        public TradingDate Next(int days)
        {
            return AddDays(days, 1);
        }

        /// <summary>
        ///   Convert a start and end date to a list of all the trading dates inbetween 
        ///   which do not fall on weekends.
        /// </summary>
        /// <param name = "startDate">The start date.</param>
        /// <param name = "endDate">The end date.</param>
        /// <returns>a list of all the trading dates inbetween</returns>
        public static List<TradingDate> DateRangeToList(TradingDate startDate, TradingDate endDate)
        {
            Condition.Requires(startDate, "startDate").IsNotNull();
            Condition.Requires(endDate, "endDate").IsNotNull();
            Condition.Requires(startDate.CompareTo(endDate)).IsLessOrEqual(0);

            var list = new List<TradingDate>();
            var date = startDate;

            while (!date.After(endDate))
            {
                list.Add(date);
                date = date.Next(1);
            }
            return list;
        }

        private static bool IsValidDate(int year, int month, int day)
        {
            if (month == 0 || month > 12 || day == 0)
                return false;

            if (day > DateTime.DaysInMonth(year, month))
                return false;

            return true;
        }

        public static TradingDate CreateInstance(DateTime param)
        {
            return new TradingDate(param);
        }

        TradingDate IParameterizedInstance<TradingDate, DateTime>.CreateInstance(DateTime param)
        {
            return CreateInstance(param);
        }

        public override string ToString()
        {
            return Date.ToShortDateString();
        }
    }
}